// NFC_11 [OpenGL Spheres Applet].nova



// Using namespace declarations.
using Library.Math;
using Library.NFC;
using Library.OpenGL;



// The applet class.
class SpheresApplet : Applet, IRun
{
   // Applet data members.
   private bool active;

   private int mouseX, mouseY;
   private bool mouseButtonDown;

   private double spinX, spinY;

   // THe view matrix.
   private Matrix4d viewMatrix;

   // An array to hold the spheres.
   private GLObject[] spheres;

   // The number of spheres.
   private uint numSpheres;

   // Animation flags.
   private bool animationEnabled, blurEnabled;

   // Sphere attributes.
   private double angleX, angleY, angleZ;
   private double separation;
   private double animationSpeed;
   private double transparency;

   // The window's OpenGL rendering context.
   private RenderingContext renderingContext;



   // Application class's "main" function.
   public static void main( String[] args )
   {
      // Output the keyboard contols.
      Stream.write( "Keyboard controls:\n"
                  + "   a = toggle animation\n"
                  + "   b = toggle motion blur\n" );

      // Create a new applet window with a new applet instance.
      AppletWindow window = new AppletWindow( new SpheresApplet( ),
                                              "NFC_11 [OpenGL Spheres Applet]",
                                              640,
                                              480 );

      // Show the applet window.
      window.show( true );

      // Process the applet window's events.
      window.processEvents( );
   }



   // Initialize the applet.
   public virtual void onInit( )
   {
      // Initialize the applet's data members.
      mouseX = mouseY = 0;
      spinX = spinY = 0;

      active = true;
      mouseButtonDown = false;

      viewMatrix = new Matrix4d( );

      numSpheres = 4;

      animationEnabled = true;
      blurEnabled = false;

      angleX = angleY = angleZ = 0;
      separation = 30.0;
      animationSpeed = 0.7;
      transparency = 0.8;


      // Create an OpenGL rendering context.
      renderingContext = OpenGL.createContext( graphics );

      // Make the new rendering context the current one.
      OpenGL.makeCurrent( graphics, renderingContext );

      // Initialize the context.
      OpenGL.glClearColor( 1.0f, 1.0f, 1.0f, 0.0f );
      OpenGL.glShadeModel( OpenGL.GL_FLAT );
      OpenGL.glClearDepth( 1.0 );
      OpenGL.glEnable( OpenGL.GL_DEPTH_TEST );

      float[ ] light_diffuse =  { 1.0f, 1.0f, 1.0f, 0.0f };
      float[ ] light_specular = { 1.0f, 1.0f, 1.0f, 0.0f };
      float[ ] mat_specular =   { 1.0f, 1.0f, 1.0f, 1.0f };
      float[ ] mat_shininess =  { 50.0f };

      OpenGL.glLightfv( OpenGL.GL_LIGHT0, OpenGL.GL_DIFFUSE, light_diffuse );
      OpenGL.glLightfv( OpenGL.GL_LIGHT0, OpenGL.GL_SPECULAR, light_specular );

      OpenGL.glMaterialfv( OpenGL.GL_FRONT_AND_BACK, OpenGL.GL_SPECULAR, mat_specular );
      OpenGL.glMaterialfv( OpenGL.GL_FRONT_AND_BACK, OpenGL.GL_SHININESS, mat_shininess );

      OpenGL.glColorMaterial( OpenGL.GL_FRONT_AND_BACK, OpenGL.GL_DIFFUSE );
      OpenGL.glEnable( OpenGL.GL_COLOR_MATERIAL );
      OpenGL.glEnable( OpenGL.GL_LIGHT0 );
      OpenGL.glEnable( OpenGL.GL_LIGHTING );
      OpenGL.glEnable( OpenGL.GL_NORMALIZE );
      OpenGL.glEnable( OpenGL.GL_CULL_FACE );


      OpenGL.glEnable( OpenGL.GL_BLEND );
      OpenGL.glBlendFunc( OpenGL.GL_SRC_ALPHA, OpenGL.GL_ONE_MINUS_SRC_ALPHA );

      OpenGL.glClearAccum( 1.0, 1.0, 1.0, 1.0 );
      OpenGL.glClear( OpenGL.GL_ACCUM_BUFFER_BIT );

      // Create the applet's spheres.
      createSpheres( );

      // Create a new thread to update the applet.
      Thread updateThread = new Thread( this, null );

      // Start the thread.
      updateThread.start( );
   }



   // Create the spheres for the scene.
   private void createSpheres( )
   {
      // Initialize to an empty array.
      spheres = new GLObject[ numSpheres ];

      // Iterate through the number of spheres.
      for ( uint loopIndex = 0; loopIndex < numSpheres; loopIndex++ )
      {
         // Create a sphere display list.
         uint displayList = OpenGL.glGenLists( 1 );
         OpenGL.glNewList( displayList, OpenGL.GL_COMPILE );
         Sphere.drawSphere( );
         OpenGL.glEndList( );

         // Create a new GLObject with the display list.
         // The display list is deleted when the object is deleted.
         spheres[ loopIndex ] = new GLObject( displayList );
      }

      // Set the colours of the spheres.
      spheres[ 0 ].setColour( 1.0, 0.1, 0.0, transparency );  // Red.
      spheres[ 1 ].setColour( 0.0, 1.0, 0.0, transparency );  // Green.
      spheres[ 2 ].setColour( 0.0, 0.3, 1.0, transparency );  // Blue.
      spheres[ 3 ].setColour( 1.0, 1.0, 0.2, transparency );  // Yellow.
   }



   // Run the applet (the update thread's method).
   public virtual Object run( Object arg )
   {
      // Run while the active flag is set.
      while ( active )
      {
         // Check if the mouse button is up.
         if ( mouseButtonDown == false )
         {
            // Spin the scene.
            rotateScene( );
         }

         // Repaint the applet.
         repaint( );
      }


      // Set the current OpenGL context to null.
      OpenGL.makeCurrent( null, null );

      // Delete the OpenGL context.
      OpenGL.deleteContext( renderingContext );

      return null;
   }



   // On key down event handler.
   public virtual void onKeyDown( uint key )
   {
      // Check for the "a" key.
      if ( key == KeyReference.VK_A )
      {
         // Toggle the animation flag.
         animationEnabled = !animationEnabled;
      }

      // Check for the "b" key.
      if ( key == KeyReference.VK_B )
      {
         // Toggle the blur flag.
         blurEnabled = !blurEnabled;

         if ( blurEnabled )
         {
            OpenGL.glClear( OpenGL.GL_ACCUM_BUFFER_BIT );
         }
      }
   }



   // On mouse button down event handler.
   public virtual void onMouseButtonDown( int button )
   {
      // Check for the left mouse button index.
      if ( button == 0 )
      {
         // Update the state of the left mouse button.
         mouseButtonDown = true;

         // Stop the cube from spinning.
         spinX = spinY = 0;
      }
   }



   // On mouse button up event handler.
   public virtual void onMouseButtonUp( int button )
   {
      // Check for the left mouse button index.
      if ( button == 0 )
      {
         // Update the state of the left mouse button.
         mouseButtonDown = false;
      }
   }



   // On mouse move event handler.
   public virtual void onMouseMove( int posX, int posY )
   {
      // Update the state of the left mouse button.
      mouseButtonDown = getKeyState( KeyReference.VK_LBUTTON );

      // Check if the mouse button is pressed.
      if ( mouseButtonDown )
      {
         spinX = (double)( posY - mouseY ) / 2.0;
         spinY = (double)( posX - mouseX ) / 2.0;

         // Orientate the scene.
         rotateScene( );
      }

      mouseX = posX;
      mouseY = posY;
   }



   // On size event handler.
   public virtual void onSize( int sizeX, int sizeY )
   {
      // Prevent a divide by zero error.
      if ( sizeY == 0 )
      {
         // Set the height equal to one.
         sizeY = 1;
      }

      // Reset the current viewport.
      OpenGL.glViewport( 0, 0, sizeX, sizeY );

      // Select the projection matrix.
      OpenGL.glMatrixMode( OpenGL.GL_PROJECTION );
      // Reset the projection matrix.
      OpenGL.glLoadIdentity( );

      // Calculate the aspect ratio of the window.
      OpenGL.gluPerspective( 45.0, (double)sizeX / (double)sizeY, 10.0, 1000.0 );

      // Select the modelview matrix.
      OpenGL.glMatrixMode( OpenGL.GL_MODELVIEW );
      // Reset the modelview matrix.
      OpenGL.glLoadIdentity( );


      if ( blurEnabled )
      {
         OpenGL.glClear( OpenGL.GL_ACCUM_BUFFER_BIT );
      }
   }



   // On paint event handler.
   public virtual void onPaint( )
   {
      // Clear the buffers.
      OpenGL.glClear( OpenGL.GL_COLOR_BUFFER_BIT | OpenGL.GL_DEPTH_BUFFER_BIT );

      // Reset the current matrix.
      OpenGL.glLoadIdentity( );

      // Move into the scene.
      OpenGL.glTranslatef( 0.0f, 0.0f, -100.0f );

      // Check the animation flag.
      if ( animationEnabled )
      {
         // Increment the animation angles.
         angleX += 0.5 * animationSpeed;
         angleY += 0.6 * animationSpeed;
         angleZ += 0.7 * animationSpeed;
      }


      //////////////////////////////////////////////
      // Calculate the common sphere orientation. //
      //////////////////////////////////////////////
      Matrix3d orientationMatrix = new Matrix3d( );

      orientationMatrix.rotateX( -angleX );
      orientationMatrix.rotateY( -angleY );
      orientationMatrix.rotateZ( -angleZ );


      // Create a new animation matrix.
      Matrix4d animationMatrix = new Matrix4d( );

      // Rotate the animation by the view matrix.
      animationMatrix.multiply( viewMatrix );

      // Initialize the animation matrix.
      animationMatrix.rotateX( angleX );
      animationMatrix.rotateY( angleY );
      animationMatrix.rotateZ( angleZ );


      // Create a matrix to position each sphere.
      Matrix4d sphereMatrix = new Matrix4d( );
      double[] position = new double[ 3 ];


      // Render the red sphere.
      sphereMatrix.multiply( animationMatrix );
      sphereMatrix.translate( separation, 0, 0 );
      sphereMatrix.transform( position );
      spheres[ 0 ].setPosition( position );
      spheres[ 0 ].setOrientation( orientationMatrix );

      // Render the green sphere.
      sphereMatrix.loadIdentity( );
      sphereMatrix.multiply( animationMatrix );
      sphereMatrix.translate( -separation, 0, 0 );
      position = { 0, 0, 0 };
      sphereMatrix.transform( position );
      spheres[ 1 ].setPosition( position );
      spheres[ 1 ].setOrientation( orientationMatrix );

      // Render the blue sphere.
      sphereMatrix.loadIdentity( );
      sphereMatrix.multiply( animationMatrix );
      sphereMatrix.translate( 0, separation, 0 );
      position = { 0, 0, 0 };
      sphereMatrix.transform( position );
      spheres[ 2 ].setPosition( position );
      spheres[ 2 ].setOrientation( orientationMatrix );

      // Render the yellow sphere.
      sphereMatrix.loadIdentity( );
      sphereMatrix.multiply( animationMatrix );
      sphereMatrix.translate( 0, -separation, 0 );
      position = { 0, 0, 0 };
      sphereMatrix.transform( position );
      spheres[ 3 ].setPosition( position );
      spheres[ 3 ].setOrientation( orientationMatrix );


      // Create a shallow copy of the spheres array.
      GLObject[] spheresCopy = spheres.copy( );

      // Depth sort the copied array of spheres into the most distant sphere first.
      // The most distant sphere has the lowest z axis value in OpenGL.
      // This is needed to ensure the correct rendering of the sphere transparencies.
      for ( uint i = 1; i < numSpheres; i++ )
      {
         for ( uint j = 0; j < numSpheres - 1; j++ )
         {
            // Get the z positions of the current two objects.
            double z1 = spheresCopy[ j ].getPositionZ( );
            double z2 = spheresCopy[ j + 1 ].getPositionZ( );

            // Compare the current two z axis values.
            if ( z1 > z2 )
            {
               // Reorder the current two objects in increasing value of z.
               GLObject hold = spheresCopy[ j ];
               spheresCopy[ j ] = spheresCopy[ j + 1 ];
               spheresCopy[ j + 1 ] = hold;
            }
         }
      }


      // Iterate through all of the spheres.
      for ( uint loopIndex = 0; loopIndex < numSpheres; loopIndex++ )
      {
         // Render the current sphere.
         spheresCopy[ loopIndex ].render( );
      }


      // Apply motion blur if enabled.
      if ( blurEnabled )
      {
         float q = 0.92f;

         OpenGL.glAccum( OpenGL.GL_MULT, q );      // Reduce (scale) the accum buffer.
         OpenGL.glAccum( OpenGL.GL_ACCUM, 1 - q ); // Add a fraction of the back buffer to the accum buffer.
         OpenGL.glAccum( OpenGL.GL_RETURN, 1 );    // Copy the accum buffer to the back buffer (current).
      }


      // Swap the window's OpenGL buffers.
      OpenGL.swapBuffers( graphics );
   }



   // On close event handler.
   public virtual void onClose( )
   {
      // Set the active flag to false to stop the rendering thread.
      active = false;
   }



   // Rotate the scene.
   private void rotateScene( )
   {
      // Create a local rotation matrix.
      Matrix4d rotationMatix = new Matrix4d( );

      // Rotate the rotation matrix.
      rotationMatix.rotateX( spinX );
      rotationMatix.rotateY( spinY );

      // Pre-multiply the scene's matrix with the rotation matrix.
      viewMatrix = rotationMatix.multiply( viewMatrix );
   }
}



class Sphere
{
   public static void drawSphere( )
   {
      // Create a new GLUqradric object.
      GLUquadric quadObject = OpenGL.gluNewQuadric( );

      OpenGL.gluQuadricDrawStyle( quadObject, OpenGL.GLU_FILL );

      OpenGL.gluQuadricNormals( quadObject, OpenGL.GLU_FLAT );

      OpenGL.gluSphere( quadObject, 20.0, 10, 5 );

      OpenGL.gluDeleteQuadric( quadObject );
   }
}